Purpose: Decode AIS message (payload) into a readable format¶
Output: Separated by fields¶
In [1]:
"""
AIS Message Decoder for Final Year Project
Purpose: Decode different types of AIS messages from NMEA format
Output: TXT format with all the navigation fields
Reference: https://gpsd.gitlab.io/gpsd/AIVDM.html#_json_ais_encoding
"""
import sys
# convert AIS character to 6-bit value
def convert_ais_char(char):
ascii_value = ord(char)
val = ascii_value - 48
if val > 40:
val = val - 8
return val
# extract bits from binary string
def extract_bits(binary_data, start_pos, bit_length):
if start_pos + bit_length > len(binary_data):
return None
bit_string = binary_data[start_pos:start_pos + bit_length]
return int(bit_string, 2)
# extract signed bits (for negative values)
def extract_signed_bits(binary_data, start_pos, bit_length):
value = extract_bits(binary_data, start_pos, bit_length)
if value is None:
return None
# check if negative (MSB is 1)
if value >= (1 << (bit_length - 1)):
value = value - (1 << bit_length)
return value
# convert payload to binary
def convert_payload_to_binary(payload):
result = ""
for char in payload:
six_bit_val = convert_ais_char(char)
binary_str = format(six_bit_val, '06b')
result += binary_str
return result
# parse NMEA sentence to get payload
def get_payload_from_nmea(sentence):
if not sentence.startswith('!AIVDM') and not sentence.startswith('!AIVDO'):
return None
parts = sentence.strip().split(',')
if len(parts) >= 6:
return parts[5]
return None
# format coordinates with hemisphere
def format_lat_lon(coordinate, is_lon=True):
if coordinate is None:
return None, None
if is_lon:
if coordinate >= 0:
hemisphere = 'E'
else:
hemisphere = 'W'
else: # latitude
if coordinate >= 0:
hemisphere = 'N'
else:
hemisphere = 'S'
return abs(coordinate), hemisphere
# extract text from 6-bit encoded field
def extract_text(binary_data, start_pos, num_chars):
text = ""
for i in range(num_chars):
char_bits = extract_bits(binary_data, start_pos + (i * 6), 6)
if char_bits is None:
break
if char_bits < 32:
ascii_char = char_bits + 64
else:
ascii_char = char_bits
if ascii_char >= 32 and ascii_char <= 126:
text += chr(ascii_char)
else:
text += ' '
return text.strip()
# main decode function
def decode_ais(nmea_sentence):
payload = get_payload_from_nmea(nmea_sentence)
if not payload:
return None
binary_data = convert_payload_to_binary(payload)
if len(binary_data) < 38:
return None
# get basic message info
msg_type = extract_bits(binary_data, 0, 6)
repeat_ind = extract_bits(binary_data, 6, 2)
mmsi = extract_bits(binary_data, 8, 30)
# initialize all variables
nav_status = None
rot = None
sog = None
pos_accuracy = None
longitude = None
latitude = None
lat_hem = None
lon_hem = None
cog = None
heading = None
utc_sec = None
raim = None
sync = None
slot = None
ship_name = None
ship_type = None
destination = None
draught = None
imo = None
callsign = None
dim_a = None
dim_b = None
dim_c = None
dim_d = None
ais_version = None
dte = None
altitude = None
aid_type = None
name_extension = None
off_position = None
gnss = None
# decode based on message type
if msg_type in [1, 2, 3] and len(binary_data) >= 168:
# Class A position reports
nav_status = extract_bits(binary_data, 38, 4)
# Rate of turn calculation - GPSD standard
rot_raw = extract_signed_bits(binary_data, 42, 8)
if rot_raw == -128:
rot = "-128.0" # Not available (default)
elif rot_raw == -127:
rot = "-720.0" # Turning left at more than 5°/30s
elif rot_raw == 127:
rot = "+127.0" # Turning right at more than 5°/30s
elif rot_raw is not None:
if rot_raw == 0:
rot = "+0.0"
else:
# GPSD formula: ROT degrees/min = (ROT_raw/4.733)^2 * sign(ROT_raw)
if rot_raw > 0:
rot_degrees = (rot_raw / 4.733) ** 2
rot = f"+{rot_degrees:.1f}"
else:
rot_degrees = (abs(rot_raw) / 4.733) ** 2
rot = f"-{rot_degrees:.1f}"
else:
rot = "+0.0"
# Speed over ground
sog_raw = extract_bits(binary_data, 50, 10)
if sog_raw == 1023:
sog = "0.0"
else:
sog = f"{sog_raw / 10.0:.1f}"
pos_accuracy = extract_bits(binary_data, 60, 1)
# Position decoding
lon_raw = extract_signed_bits(binary_data, 61, 28)
lat_raw = extract_signed_bits(binary_data, 89, 27)
# check for valid longitude
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
# check for valid latitude
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
# Course over ground
cog_raw = extract_bits(binary_data, 116, 12)
if cog_raw >= 3600:
cog = "360.0"
else:
cog = f"{cog_raw / 10.0:.1f}"
# True heading
hdg_raw = extract_bits(binary_data, 128, 9)
if hdg_raw == 511:
heading = 511
else:
heading = hdg_raw
# UTC second
utc_raw = extract_bits(binary_data, 137, 6)
if utc_raw >= 60:
utc_sec = 60
else:
utc_sec = utc_raw
# Communication state
raim = extract_bits(binary_data, 148, 1)
sync = extract_bits(binary_data, 149, 2)
slot = extract_bits(binary_data, 151, 3)
elif msg_type == 4 and len(binary_data) >= 168:
# Base station report
pos_accuracy = extract_bits(binary_data, 78, 1)
lon_raw = extract_signed_bits(binary_data, 79, 28)
lat_raw = extract_signed_bits(binary_data, 107, 27)
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
raim = extract_bits(binary_data, 148, 1)
elif msg_type == 5 and len(binary_data) >= 424:
# Static & voyage related data
ais_version = extract_bits(binary_data, 38, 2)
imo = extract_bits(binary_data, 40, 30)
callsign = extract_text(binary_data, 70, 7)
ship_name = extract_text(binary_data, 112, 20)
ship_type = extract_bits(binary_data, 232, 8)
dim_a = extract_bits(binary_data, 240, 9)
dim_b = extract_bits(binary_data, 249, 9)
dim_c = extract_bits(binary_data, 258, 6)
dim_d = extract_bits(binary_data, 264, 6)
pos_accuracy = extract_bits(binary_data, 270, 4)
draught_raw = extract_bits(binary_data, 294, 8)
if draught_raw is not None and draught_raw > 0:
draught = f"{draught_raw / 10.0:.1f}"
destination = extract_text(binary_data, 302, 20)
dte = extract_bits(binary_data, 422, 1)
elif msg_type in [6, 8] and len(binary_data) >= 88:
# Binary addressed message (6) & Binary broadcast message (8)
# Only extracting basic fields, payload data not decoded
pass
elif msg_type == 7 and len(binary_data) >= 72:
# Binary acknowledge
# Contains MMSI acknowledgements, basic structure only
pass
elif msg_type == 9 and len(binary_data) >= 168:
# SAR aircraft position
altitude_raw = extract_bits(binary_data, 38, 12)
if altitude_raw != 4095:
altitude = altitude_raw
sog_raw = extract_bits(binary_data, 50, 10)
if sog_raw == 1023:
sog = "0.0"
else:
sog = f"{sog_raw / 10.0:.1f}"
pos_accuracy = extract_bits(binary_data, 60, 1)
lon_raw = extract_signed_bits(binary_data, 61, 28)
lat_raw = extract_signed_bits(binary_data, 89, 27)
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
cog_raw = extract_bits(binary_data, 116, 12)
if cog_raw >= 3600:
cog = "360.0"
else:
cog = f"{cog_raw / 10.0:.1f}"
utc_sec = extract_bits(binary_data, 128, 6)
dte = extract_bits(binary_data, 142, 1)
raim = extract_bits(binary_data, 147, 1)
elif msg_type == 10 and len(binary_data) >= 72:
# UTC & date inquiry
# Request for UTC, basic structure only
pass
elif msg_type == 11 and len(binary_data) >= 168:
# UTC and date response
pos_accuracy = extract_bits(binary_data, 78, 1)
lon_raw = extract_signed_bits(binary_data, 79, 28)
lat_raw = extract_signed_bits(binary_data, 107, 27)
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
raim = extract_bits(binary_data, 148, 1)
elif msg_type in [12, 14] and len(binary_data) >= 72:
# Addressed safety related message (12) & Safety related broadcast (14)
# Text message, basic structure only
pass
elif msg_type == 13 and len(binary_data) >= 72:
# Safety related acknowledgement
# Contains MMSI acknowledgements, basic structure only
pass
elif msg_type == 15 and len(binary_data) >= 88:
# Interrogation
# Request for specific message types, basic structure only
pass
elif msg_type == 16 and len(binary_data) >= 96:
# Assignment mode command
# Station assignment, basic structure only
pass
elif msg_type == 17 and len(binary_data) >= 80:
# DGNSS broadcast binary message
lon_raw = extract_signed_bits(binary_data, 40, 18)
lat_raw = extract_signed_bits(binary_data, 58, 17)
if lon_raw != 0x1A838:
lon_degrees = lon_raw / 600.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0xD548:
lat_degrees = lat_raw / 600.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
elif msg_type == 18 and len(binary_data) >= 168:
# Class B position report
sog_raw = extract_bits(binary_data, 46, 10)
if sog_raw == 1023:
sog = "0.0"
else:
sog = f"{sog_raw / 10.0:.1f}"
pos_accuracy = extract_bits(binary_data, 56, 1)
lon_raw = extract_signed_bits(binary_data, 57, 28)
lat_raw = extract_signed_bits(binary_data, 85, 27)
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
cog_raw = extract_bits(binary_data, 112, 12)
if cog_raw >= 3600:
cog = "360.0"
else:
cog = f"{cog_raw / 10.0:.1f}"
hdg_raw = extract_bits(binary_data, 124, 9)
if hdg_raw == 511:
heading = 511
else:
heading = hdg_raw
utc_raw = extract_bits(binary_data, 133, 6)
if utc_raw >= 60:
utc_sec = 60
else:
utc_sec = utc_raw
raim = extract_bits(binary_data, 147, 1)
sync = extract_bits(binary_data, 149, 2)
slot = extract_bits(binary_data, 151, 3)
elif msg_type == 19 and len(binary_data) >= 312:
# Extended Class B
sog_raw = extract_bits(binary_data, 46, 10)
if sog_raw == 1023:
sog = "0.0"
else:
sog = f"{sog_raw / 10.0:.1f}"
pos_accuracy = extract_bits(binary_data, 56, 1)
lon_raw = extract_signed_bits(binary_data, 57, 28)
lat_raw = extract_signed_bits(binary_data, 85, 27)
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
cog_raw = extract_bits(binary_data, 112, 12)
if cog_raw >= 3600:
cog = "360.0"
else:
cog = f"{cog_raw / 10.0:.1f}"
hdg_raw = extract_bits(binary_data, 124, 9)
if hdg_raw == 511:
heading = 511
else:
heading = hdg_raw
utc_raw = extract_bits(binary_data, 133, 6)
if utc_raw >= 60:
utc_sec = 60
else:
utc_sec = utc_raw
ship_name = extract_text(binary_data, 143, 20)
ship_type = extract_bits(binary_data, 263, 8)
dim_a = extract_bits(binary_data, 271, 9)
dim_b = extract_bits(binary_data, 280, 9)
dim_c = extract_bits(binary_data, 289, 6)
dim_d = extract_bits(binary_data, 295, 6)
raim = extract_bits(binary_data, 305, 1)
dte = extract_bits(binary_data, 306, 1)
elif msg_type == 20 and len(binary_data) >= 72:
# Data link management message
# Slot management, basic structure only
pass
elif msg_type == 21 and len(binary_data) >= 272:
# Aid to navigation
aid_type = extract_bits(binary_data, 38, 5)
ship_name = extract_text(binary_data, 43, 20)
pos_accuracy = extract_bits(binary_data, 163, 1)
lon_raw = extract_signed_bits(binary_data, 164, 28)
lat_raw = extract_signed_bits(binary_data, 192, 27)
if lon_raw != 0x6791AC0:
lon_degrees = lon_raw / 600000.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0x3412140:
lat_degrees = lat_raw / 600000.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
dim_a = extract_bits(binary_data, 219, 9)
dim_b = extract_bits(binary_data, 228, 9)
dim_c = extract_bits(binary_data, 237, 6)
dim_d = extract_bits(binary_data, 243, 6)
utc_sec = extract_bits(binary_data, 253, 6)
off_position = extract_bits(binary_data, 259, 1)
raim = extract_bits(binary_data, 268, 1)
if len(binary_data) >= 360:
name_extension = extract_text(binary_data, 272, 14)
elif msg_type == 22 and len(binary_data) >= 168:
# Channel management
# Channel assignment, basic structure only
pass
elif msg_type == 23 and len(binary_data) >= 160:
# Group assignment command
# Station assignment, basic structure only
pass
elif msg_type == 24 and len(binary_data) >= 168:
# Static data report
part_num = extract_bits(binary_data, 38, 2)
if part_num == 0:
# Part A - vessel name
ship_name = extract_text(binary_data, 40, 20)
elif part_num == 1:
# Part B - static data
ship_type = extract_bits(binary_data, 40, 8)
callsign = extract_text(binary_data, 90, 7)
dim_a = extract_bits(binary_data, 132, 9)
dim_b = extract_bits(binary_data, 141, 9)
dim_c = extract_bits(binary_data, 150, 6)
dim_d = extract_bits(binary_data, 156, 6)
elif msg_type == 25 and len(binary_data) >= 40:
# Single slot binary message
# Application specific, basic structure only
pass
elif msg_type == 26 and len(binary_data) >= 60:
# Multiple slot binary message
# Application specific, basic structure only
pass
elif msg_type == 27 and len(binary_data) >= 96:
# Long range AIS
pos_accuracy = extract_bits(binary_data, 38, 1)
raim = extract_bits(binary_data, 39, 1)
nav_status = extract_bits(binary_data, 40, 4)
lon_raw = extract_signed_bits(binary_data, 44, 18)
lat_raw = extract_signed_bits(binary_data, 62, 17)
if lon_raw != 0x1A838:
lon_degrees = lon_raw / 600.0
longitude, lon_hem = format_lat_lon(lon_degrees, True)
if longitude is not None:
longitude = f"{longitude:.7f}"
if lat_raw != 0xD548:
lat_degrees = lat_raw / 600.0
latitude, lat_hem = format_lat_lon(lat_degrees, False)
if latitude is not None:
latitude = f"{latitude:.7f}"
sog_raw = extract_bits(binary_data, 79, 6)
if sog_raw == 63:
sog = "0.0"
else:
sog = f"{sog_raw:.1f}"
cog_raw = extract_bits(binary_data, 85, 9)
if cog_raw >= 360:
cog = "360.0"
else:
cog = f"{cog_raw:.1f}"
gnss = extract_bits(binary_data, 94, 1)
# only return decoded values for valid AIS message types (1-27)
if msg_type >= 1 and msg_type <= 27:
return [
msg_type,
repeat_ind,
mmsi,
nav_status,
rot,
sog,
pos_accuracy,
longitude,
lon_hem,
latitude,
lat_hem,
cog,
heading,
utc_sec,
sync,
slot,
raim,
ship_name,
ship_type,
callsign,
destination,
draught,
imo,
dim_a,
dim_b,
dim_c,
dim_d,
ais_version,
dte,
altitude,
aid_type,
name_extension,
off_position,
gnss
]
else:
# invalid message type
return None
# convert values to CSV format
def make_csv_line(decoded_values):
if not decoded_values:
return None
csv_parts = []
for value in decoded_values:
if value is None:
csv_parts.append('0')
else:
csv_parts.append(str(value))
return ','.join(csv_parts)
# process the input file
def process_ais_file(input_filename, output_filename=None):
if output_filename is None:
output_filename = input_filename.replace('.txt', '_decoded.txt')
try:
input_file = open(input_filename, 'r')
output_file = open(output_filename, 'w')
# write header
header = "message_type,repeat_indicator,mmsi,navigation_status,rate_of_turn,speed_over_ground,position_accuracy,longitude,lon_hemisphere,latitude,lat_hemisphere,course_over_ground,true_heading,utc_second,sync_state,slot_timeout,raim_flag,ship_name,ship_type,callsign,destination,draught,imo,dim_a,dim_b,dim_c,dim_d,ais_version,dte,altitude,aid_type,name_extension,off_position,gnss"
output_file.write(header + '\n')
total_messages = 0
decoded_messages = 0
invalid_messages = 0
message_types = {}
invalid_types = {}
messages_with_position = 0
valid_without_position = 0
for line in input_file:
line = line.strip()
if not line:
continue
total_messages += 1
# first check if message type is valid by extracting it
payload = get_payload_from_nmea(line)
if payload:
binary_data = convert_payload_to_binary(payload)
if len(binary_data) >= 6:
msg_type_check = extract_bits(binary_data, 0, 6)
if msg_type_check is not None and (msg_type_check < 1 or msg_type_check > 27):
# invalid message type
invalid_messages += 1
if msg_type_check in invalid_types:
invalid_types[msg_type_check] += 1
else:
invalid_types[msg_type_check] = 1
continue
result = decode_ais(line)
if result:
msg_type = result[0]
if msg_type in message_types:
message_types[msg_type] += 1
else:
message_types[msg_type] = 1
# check if message has position data
if result[7] is not None and result[9] is not None:
messages_with_position += 1
else:
valid_without_position += 1
csv_line = make_csv_line(result)
if csv_line:
output_file.write(csv_line + '\n')
decoded_messages += 1
input_file.close()
output_file.close()
# print summary
print(f"Total messages processed: {total_messages}")
print(f"Successfully decoded: {decoded_messages}")
print(f"Invalid/non-standard message types: {invalid_messages}")
print(f"\nValid messages with position data: {messages_with_position}")
print(f"Valid messages without position data: {valid_without_position}")
print(f"\nValid message type summary:")
for msg_type in sorted(message_types.keys()):
print(f" Type {msg_type}: {message_types[msg_type]} messages")
if invalid_types:
print(f"\nInvalid/non-standard message types found:")
for msg_type in sorted(invalid_types.keys()):
print(f" Type {msg_type}: {invalid_types[msg_type]} messages")
print(f"\nDecoded data saved to: {output_filename}")
except FileNotFoundError:
print(f"Error: Could not find file {input_filename}")
except Exception as error:
print(f"Error occurred: {error}")
# debug function to test single message
def debug_single_message(nmea_msg):
payload = get_payload_from_nmea(nmea_msg)
if not payload:
print("Could not parse NMEA message")
return
binary = convert_payload_to_binary(payload)
print(f"Payload: {payload}")
print(f"Binary length: {len(binary)} bits")
msg_type = extract_bits(binary, 0, 6)
repeat = extract_bits(binary, 6, 2)
mmsi = extract_bits(binary, 8, 30)
print(f"Message type: {msg_type}")
print(f"Repeat: {repeat}")
print(f"MMSI: {mmsi}")
if msg_type in [1, 2, 3]:
print("Class A position report detected")
nav_stat = extract_bits(binary, 38, 4)
rot_raw = extract_signed_bits(binary, 42, 8)
sog_raw = extract_bits(binary, 50, 10)
pos_acc = extract_bits(binary, 60, 1)
lon_raw = extract_signed_bits(binary, 61, 28)
lat_raw = extract_signed_bits(binary, 89, 27)
print(f"Navigation status: {nav_stat}")
print(f"ROT raw: {rot_raw}")
print(f"SOG raw: {sog_raw}")
print(f"Position accuracy: {pos_acc}")
print(f"Longitude raw: {lon_raw}")
print(f"Latitude raw: {lat_raw}")
# main program
if __name__ == "__main__":
# test with sample message
test_nmea = "!AIVDM,1,1,,A,38IFDN0Ohj7JvbN0fABtpbJ401w@,0*69"
print("Testing decoder with sample message:")
print(f"Input: {test_nmea}")
debug_single_message(test_nmea)
decoded = decode_ais(test_nmea)
if decoded:
print(f"Decoded result: {make_csv_line(decoded)}")
else:
print("Decoding failed!")
print("\n" + "="*60 + "\n")
# process full file
input_path = r"C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample"
output_path = r"C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample_Python_AIS_Decoder_081125"
print(f"Processing AIS messages from: {input_path}")
process_ais_file(input_path, output_path)
Testing decoder with sample message: Input: !AIVDM,1,1,,A,38IFDN0Ohj7JvbN0fABtpbJ401w@,0*69 Payload: 38IFDN0Ohj7JvbN0fABtpbJ401w@ Binary length: 168 bits Message type: 3 Repeat: 0 MMSI: 563451000 Class A position report detected Navigation status: 0 ROT raw: 127 SOG raw: 50 Position accuracy: 0 Longitude raw: 62256463 Latitude raw: 758091 Decoded result: 3,0,563451000,0,+127.0,5.0,0,103.7607717,E,1.2634850,N,329.8,333,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ============================================================ Processing AIS messages from: C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample Total messages processed: 85194 Successfully decoded: 83948 Invalid/non-standard message types: 1232 Valid messages with position data: 77192 Valid messages without position data: 6756 Valid message type summary: Type 1: 61418 messages Type 2: 2018 messages Type 3: 8817 messages Type 4: 4074 messages Type 5: 2240 messages Type 6: 458 messages Type 7: 5 messages Type 8: 1688 messages Type 9: 30 messages Type 10: 12 messages Type 11: 38 messages Type 12: 25 messages Type 13: 8 messages Type 14: 4 messages Type 15: 6 messages Type 16: 45 messages Type 17: 343 messages Type 18: 1387 messages Type 19: 219 messages Type 20: 323 messages Type 21: 404 messages Type 22: 29 messages Type 23: 6 messages Type 24: 338 messages Type 25: 2 messages Type 26: 4 messages Type 27: 7 messages Invalid/non-standard message types found: Type 0: 713 messages Type 28: 1 messages Type 29: 4 messages Type 31: 2 messages Type 32: 33 messages Type 33: 70 messages Type 34: 47 messages Type 35: 19 messages Type 36: 16 messages Type 37: 12 messages Type 38: 5 messages Type 40: 42 messages Type 41: 3 messages Type 42: 1 messages Type 43: 8 messages Type 45: 4 messages Type 46: 1 messages Type 47: 8 messages Type 48: 40 messages Type 49: 16 messages Type 50: 23 messages Type 51: 17 messages Type 52: 21 messages Type 53: 18 messages Type 54: 1 messages Type 55: 4 messages Type 56: 32 messages Type 58: 1 messages Type 59: 6 messages Type 60: 2 messages Type 62: 5 messages Type 63: 57 messages Decoded data saved to: C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample_Python_AIS_Decoder_081125
In [7]:
import pandas as pd
import folium
from IPython.display import display
# Load data
decoded_file = r"C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\NMEA_Decoded.txt"
df = pd.read_csv(decoded_file)
# convert to numeric and fix hemisphere if needed
df["longitude"] = pd.to_numeric(df["longitude"], errors="coerce")
df["latitude"] = pd.to_numeric(df["latitude"], errors="coerce")
if "lon_hemisphere" in df.columns:
df.loc[df["lon_hemisphere"].fillna("").str.upper() == "W", "longitude"] *= -1
if "lat_hemisphere" in df.columns:
df.loc[df["lat_hemisphere"].fillna("").str.upper() == "S", "latitude"] *= -1
# drop invalid coords
df = df.dropna(subset=["longitude", "latitude"])
df = df[(df["longitude"] != 0) & (df["latitude"] != 0)]
# keep last message per MMSI (latest timestamp)
df_plot = df.drop_duplicates(subset=["mmsi"], keep="last").copy()
# center map
center_lat, center_lon = df_plot["latitude"].mean(), df_plot["longitude"].mean()
m = folium.Map(location=[center_lat, center_lon], zoom_start=10)
# add markers with popup
for _, row in df_plot.iterrows():
popup_html = (
f"<b>MMSI:</b> {row['mmsi']}<br>"
f"<b>SOG:</b> {row.get('speed_over_ground','')} km<br>"
f"<b>COG:</b> {row.get('course_over_ground','')}°<br>"
f"<b>Heading:</b> {row.get('true_heading','')}°<br>"
f"<b>Lat:</b> {row['latitude']:.5f}°<br>"
f"<b>Lon:</b> {row['longitude']:.5f}°"
)
folium.CircleMarker(
location=[row["latitude"], row["longitude"]],
radius=5,
color="blue",
fill=True,
fill_opacity=0.8,
popup=folium.Popup(popup_html, max_width=300)
).add_to(m)
display(m)
# can save as html ltr on if needed:
# m.save("ais_map.html")
Make this Notebook Trusted to load map: File -> Trust Notebook